﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace OpenRiaServices.DomainServices.Tools
{
    /// <summary>
    /// Utility class for generating notification partial methods and method calls. 
    /// These methods are of the form: 
    /// partial void OnCreated(), OnPropertyNameCalled/Calling(args...), OnCustomMethodInvoked/Invoking(args ...)
    /// </summary>
    internal class NotificationMethodGenerator
    {
        // dictionary of the form: Dic[Created, CodeInvokeExpression for 'this.OnCreated()']
        private readonly Dictionary<string, CodeMethodInvokeExpression> methodInvokeExpressions = new Dictionary<string, CodeMethodInvokeExpression>();

        // dictionary of the form: Dic[Created, CodeSnippetTypeMember for 'partial void OnCreated();'
        private readonly Dictionary<string, CodeSnippetTypeMember> partialMethodSnippets = new Dictionary<string, CodeSnippetTypeMember>();

        private const string IndentString = "    "; // 4 spaces
        private readonly string indent = string.Empty;

        private readonly string createdBaseName = "Created";

        private readonly bool isCSharp;

        private const IndentationLevel DefaultIndentLevel = IndentationLevel.Namespace;

        private readonly CodeDomClientCodeGenerator proxyGenerator;

        /// <summary>
        /// Initializes a new instance of the <see cref="NotificationMethodGenerator"/> class.
        /// </summary>
        /// <param name="proxyGenerator">The current <see cref="CodeDomClientCodeGenerator"/>.</param>
        public NotificationMethodGenerator(CodeDomClientCodeGenerator proxyGenerator) :
            this(proxyGenerator, DefaultIndentLevel)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotificationMethodGenerator"/> class.
        /// </summary>
        /// <param name="proxyGenerator">The current <see cref="CodeDomClientCodeGenerator"/>.</param>
        /// <param name="indentLevel">The indentation level for the code to write.</param>
        public NotificationMethodGenerator(CodeDomClientCodeGenerator proxyGenerator, IndentationLevel indentLevel)
        {
            this.proxyGenerator = proxyGenerator;
            this.isCSharp = proxyGenerator.IsCSharp;
            int level = (int)indentLevel;
            if (level < 0)
            {
                level = (int)DefaultIndentLevel;
            }

            while (level-- > 0)
            {
                this.indent += IndentString;
            }

            this.AddMethodFor("Created", Resource.CommentOnCreated); // add default partial method.
        }

        /// <summary>
        /// Known method invoke expression of the form:
        /// this.OnCreated();
        /// </summary>
        /// <returns>Returns the invoke expression.</returns>
        public CodeMethodInvokeExpression OnCreatedMethodInvokeExpression
        {
            get
            {
                return this.methodInvokeExpressions[this.createdBaseName];
            }
        }

        /// <summary>
        /// Gets all generated partial name snippets generated by the AddMethodFor methods, in the form:
        /// #region [region comment]
        /// partial void OnMethodCalled();
        /// partial void OnMethodCalling();
        /// #endregion
        /// </summary>
        public CodeTypeMemberCollection PartialMethodsSnippetBlock
        {
            get
            {
                // #endregion ends up serialized in the same line as the CodeObject that owns it.
                // we need to add a new line and also indentation.
                string endRegionCR = "\r\n" + this.indent;
                CodeSnippetTypeMember regionStart = new CodeSnippetTypeMember();
                CodeSnippetTypeMember regionEnd = new CodeSnippetTypeMember(endRegionCR);

                regionStart.StartDirectives.Add(new CodeRegionDirective(CodeRegionMode.Start, Resource.Region_ExtensibilityMethodDefinitions));
                regionEnd.EndDirectives.Add(new CodeRegionDirective(CodeRegionMode.End, null));

                List<CodeSnippetTypeMember> values = new List<CodeSnippetTypeMember>(this.partialMethodSnippets.Values);

                values.Insert(0, regionStart);
                values.Insert(values.Count, regionEnd);

                return new CodeTypeMemberCollection(values.ToArray());
            }
        }

        /// <summary>
        /// Adds a method invoke expression and a partial method definition based on the specified base method name
        /// to the internal method collection.
        /// </summary>
        /// <param name="baseMethodName">base method name w/o the On prefix (like Created for OnCreated)</param>
        /// <param name="comments">the comments for the partial property definition</param>
        public void AddMethodFor(string baseMethodName, string comments)
        {
            CodeParameterDeclarationExpressionCollection parameters = new CodeParameterDeclarationExpressionCollection();
            this.AddMethodFor(baseMethodName, parameters, comments);
        }

        /// <summary>
        /// Adds a method invoke expression and a partial method definition based on the specified base method name
        /// to the internal method collection.
        /// </summary>
        /// <param name="baseMethodName">base method name w/o the On prefix (like Created for OnCreated)</param>
        /// <param name="parameterDeclaration">parameter declaration for the only param of the method to be generated</param>
        /// <param name="comments">the comments for the partial property definition</param>
        public void AddMethodFor(string baseMethodName, CodeParameterDeclarationExpression parameterDeclaration, string comments)
        {
            CodeParameterDeclarationExpressionCollection parameters = new CodeParameterDeclarationExpressionCollection();

            if (parameterDeclaration != null)
            {
                parameters.Add(parameterDeclaration);
            }

            this.AddMethodFor(baseMethodName, parameters, comments);
        }

        /// <summary>
        /// Adds a method invoke expression and a partial method definition based on the specified base method name
        /// to the internal method collection.
        /// </summary>
        /// <param name="baseMethodName">base method name w/o the On prefix (like Created for OnCreated)</param>
        /// <param name="parameters">if provided, the parameters for the method to be generated</param>
        /// <param name="comments">the comments for the partial property definition</param>
        public void AddMethodFor(string baseMethodName, CodeParameterDeclarationExpressionCollection parameters, string comments)
        {
            Debug.Assert(!string.IsNullOrEmpty(baseMethodName), "Unexpected null or empty base method name!");

            if (!string.IsNullOrEmpty(baseMethodName))
            {
                if (!this.methodInvokeExpressions.ContainsKey(baseMethodName))
                {
                    string methodName = string.Concat("On", baseMethodName);

                    List<CodeArgumentReferenceExpression> args = new List<CodeArgumentReferenceExpression>();

                    if (parameters != null && parameters.Count > 0)
                    {
                        foreach (CodeParameterDeclarationExpression paramDeclaration in parameters)
                        {
                            args.Add(new CodeArgumentReferenceExpression(paramDeclaration.Name));
                        }
                    }

                    // Create method call.
                    // OnMethod(arg1, arg2);
                    this.methodInvokeExpressions.Add(baseMethodName, new CodeMethodInvokeExpression(
                                                                                        new CodeThisReferenceExpression(),
                                                                                        methodName,
                                                                                        args.ToArray()));

                    // Create method declaration.
                    // partial void OnMethod(Type1 param1, Type2 param2);
                    CodeSnippetTypeMember codeSnippet = this.CreateNotificationPartialMethod(baseMethodName, parameters);

                    if (!string.IsNullOrEmpty(comments))
                    {
                        // Add comment on method declaration.
                        codeSnippet.Comments.AddRange(CodeGenUtilities.GetDocComments(comments, this.isCSharp));
                    }

                    this.partialMethodSnippets.Add(baseMethodName, codeSnippet);
                }
            }
        }

        /// <summary>
        /// Gets a CodeMethodStatement from the generated CodeMethodInvokeExpression for the specified method/property name.
        /// </summary>
        /// <param name="baseMethodName">The name of the method/property for which a notification method was generated</param>
        /// <returns>Returns the expression statement.</returns>
        public CodeExpressionStatement GetMethodInvokeExpressionStatementFor(string baseMethodName)
        {
            Debug.Assert(!string.IsNullOrEmpty(baseMethodName), "Unexpected empty or null baseMethodName name!");
            Debug.Assert(this.methodInvokeExpressions.Keys.Contains<string>(baseMethodName), string.Concat("No method has been added for \'", baseMethodName, "\' base name"));

            CodeExpressionStatement statement = null;

            if (!string.IsNullOrEmpty(baseMethodName) && this.methodInvokeExpressions.Keys.Contains<string>(baseMethodName))
            {
                statement = new CodeExpressionStatement(this.methodInvokeExpressions[baseMethodName]);
            }

            return statement;
        }


        /// <summary>
        /// Generates a notification partial method for the specified method, this is of the form
        /// OnCreated(args...) for the Created(args...) method.
        /// </summary>
        /// <param name="methodName">The name of the method to create a notification method for.</param>
        /// <param name="parameters">the method parameters, if any.</param>
        /// <returns>Code snippet for the notification partial method.</returns>
        private CodeSnippetTypeMember CreateNotificationPartialMethod(string methodName, CodeParameterDeclarationExpressionCollection parameters)
        {
            CodeMemberMethod method = new CodeMemberMethod();
            method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            method.Name = "On" + methodName;
            method.Parameters.AddRange(parameters);

            if (this.proxyGenerator.ClientProxyCodeGenerationOptions.UseFullTypeNames)
            {
                foreach (CodeParameterDeclarationExpression paramExp in parameters.Cast<CodeParameterDeclarationExpression>())
                {
                    SetGlobalTypeReference(paramExp.Type);
                }
            }

            StringBuilder snippet = null;

            using (CodeDomProvider provider = CodeDomProvider.CreateProvider(this.isCSharp ? "CSharp" : "VisualBasic"))
            {
                using (StringWriter snippetWriter = new StringWriter(System.Globalization.CultureInfo.CurrentCulture))
                {
                    provider.GenerateCodeFromMember(method, snippetWriter, new CodeGeneratorOptions());
                    snippet = snippetWriter.GetStringBuilder();
                }
            }

            // replace 'public' with 'partial' - partial methods cannot be public.
            // observe we replace 'ublic' only to get the proper language keyword capitalization.
            snippet.Replace("\r\n", string.Empty);
            snippet.Replace("ublic", "artial", 1, "ublic".Length);

            if (this.isCSharp)
            {
                int idx = snippet.ToString().LastIndexOf(')');
                snippet.Remove(idx + 1, snippet.Length - idx - 1);
                snippet.Append(";");
            }
            else // VB.net
            {
                snippet.Insert(0, "Private ");
                int idx = snippet.ToString().IndexOf("End Sub", StringComparison.Ordinal);
                snippet.Insert(idx, string.Concat("\r\n", this.indent));
            }

            snippet.Insert(0, this.indent);

            return new CodeSnippetTypeMember(snippet.ToString());
        }

        /// <summary>
        /// Processes a <see cref="CodeTypeReference"/> and sets its <see cref="CodeTypeReferenceOptions"/>
        /// such that global type references will be emitted.
        /// </summary>
        /// <param name="codeTypeReference">The <see cref="CodeTypeReference"/> to update.</param>
        private static void SetGlobalTypeReference(CodeTypeReference codeTypeReference)
        {
            codeTypeReference.Options = CodeTypeReferenceOptions.GlobalReference;

            foreach (CodeTypeReference typeArg in codeTypeReference.TypeArguments)
            {
                SetGlobalTypeReference(typeArg);
            }

            if (codeTypeReference.ArrayElementType != null)
            {
                SetGlobalTypeReference(codeTypeReference.ArrayElementType);
            }
        }
    }
}
